﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using PostSharp.CodeWeaver;
using PostSharp.CodeModel;
using PostSharp.Collections;
using System.ComponentModel;
using System.Reflection;
using PostSharp.CodeModel.TypeSignatures;

namespace PropFu
{
    internal class NotifyPropertyChangedAdvice : IAdvice
    {
        private readonly MethodDefDeclaration _getMethod;
        
        private readonly MethodDefDeclaration _setMethod;

        public NotifyPropertyChangedAdvice(MethodDefDeclaration getMethod, MethodDefDeclaration setMethod)
        {
            _getMethod = getMethod;
            _setMethod = setMethod;
        }

        public int Priority 
        { 
            get { return 0; } 
        }

        public bool RequiresWeave( WeavingContext context )
        {
            return true;
        }

        public void Weave( WeavingContext context, InstructionBlock block )
        {
            switch ( context.JoinPoint.JoinPointKind )
            {
                case JoinPointKinds.BeforeMethodBody:
                    this.WeaveEntry( context, block );
                    break;

                case JoinPointKinds.AfterMethodBodySuccess:
                    this.WeaveExit( context, block );
                    break;

                default:
                    throw new ArgumentException(string.Format( "Unexpected join point kind: {0}", context.JoinPoint.JoinPointKind ) );
            }
        }

        private void WeaveEntry(WeavingContext context, InstructionBlock block)
        {
            InstructionSequence entrySequence = context.Method.MethodBody.CreateInstructionSequence();
            block.AddInstructionSequence(entrySequence, NodePosition.Before, null);

            InstructionWriter iw = context.InstructionWriter;

            iw.AttachInstructionSequence(entrySequence);
            iw.EmitSymbolSequencePoint(SymbolSequencePoint.Hidden);

            iw.EmitInstruction(OpCodeNumber.Ldarg_1);
            iw.EmitInstruction(OpCodeNumber.Ldarg_0);
            iw.EmitInstructionMethod(OpCodeNumber.Call, this._getMethod);

            InstructionSequence valuesAreEqualSequence = context.Method.MethodBody.CreateInstructionSequence();
            block.AddInstructionSequence(valuesAreEqualSequence, NodePosition.After, iw.CurrentInstructionSequence);

            ITypeSignature getMethodReturnTypeSignature = _getMethod.ReturnParameter.ParameterType;

            if (getMethodReturnTypeSignature is GenericParameterTypeSignature)
            {
            	throw new Exception("PropFu does not support properties returning generic types");
            }

            Type getMethodSystemType = getMethodReturnTypeSignature.GetSystemType(null, null);

            MethodInfo equalityOperator = getMethodSystemType.GetMethod("op_Equality");

            if (equalityOperator == null)
            {
                iw.EmitInstruction(OpCodeNumber.Ceq);
            }
            else
            {
                IMethod methodEquality = this._setMethod.Module.FindMethod(equalityOperator, BindingOptions.Default);
                iw.EmitInstructionMethod(OpCodeNumber.Call, methodEquality);
            }

            iw.EmitBranchingInstruction(OpCodeNumber.Brfalse, valuesAreEqualSequence);
            iw.EmitInstruction(OpCodeNumber.Ret);

            iw.DetachInstructionSequence();

            iw.AttachInstructionSequence(valuesAreEqualSequence);
            iw.EmitSymbolSequencePoint(SymbolSequencePoint.Hidden);

            iw.EmitInstruction(OpCodeNumber.Nop);

            iw.DetachInstructionSequence();
        }

        private void WeaveExit(WeavingContext context, InstructionBlock block)
        {
            InstructionSequence exitSequence = context.Method.MethodBody.CreateInstructionSequence();
            block.AddInstructionSequence(exitSequence, NodePosition.Before, null);

            InstructionWriter iw = context.InstructionWriter;

            iw.AttachInstructionSequence(exitSequence);
            iw.EmitSymbolSequencePoint(SymbolSequencePoint.Hidden);

            MethodDefDeclaration onPropertyChangedMethod = FindOnPropertyChangedMethodFromContext(context);

            iw.EmitInstruction(OpCodeNumber.Ldarg_0);
            iw.EmitInstructionString(OpCodeNumber.Ldstr, _getMethod.Name.Substring(4));

            MethodBase systemMethod = onPropertyChangedMethod.GetSystemMethod(null, null, BindingOptions.Default);
            IMethod boundMethod = this._setMethod.Module.FindMethod(systemMethod, BindingOptions.Default);

            iw.EmitInstructionMethod(OpCodeNumber.Callvirt, boundMethod);
                        
            iw.DetachInstructionSequence();
        }

        private static MethodDefDeclaration FindOnPropertyChangedMethodFromContext(WeavingContext context)
        {
            MethodDefDeclaration onPropertyChangedMethod = null;
            TypeDefDeclaration currentType = context.Method.DeclaringType;

            do
            {
                onPropertyChangedMethod = FindOnPropertyChangedMethod(currentType);

                if (onPropertyChangedMethod == null)
                {
                    currentType = GetTypeDeclarationOfBase(currentType);
                }

            } while (onPropertyChangedMethod == null && currentType != null);

            if (onPropertyChangedMethod == null)
            {
                throw new Exception("PropFu was unable to find a method marked with the OnPropertyChangedAttribute in this class' hierarchy: " + context.Method.DeclaringType.Name);
            }

            if (onPropertyChangedMethod.Parameters.Count != 1 ||
                onPropertyChangedMethod.Parameters[0].ParameterType.GetSystemType(null, null) != typeof(string))
            {
                throw new Exception("PropFu found a method marked with OnNotifyPropertyChanged with the incorrect signature.  The method must take one string parameter only: " + context.Method.DeclaringType.Name + "::" + context.Method.Name );
            }

            return onPropertyChangedMethod;
        }

        private static TypeDefDeclaration GetTypeDeclarationOfBase(TypeDefDeclaration type)
        {
            if(type.BaseType == null)
                return null;

            TypeDefDeclaration baseTypeDefDeclaration = type.BaseType as TypeDefDeclaration;

            if (baseTypeDefDeclaration == null)
            {
                TypeRefDeclaration baseAsTypeRefDeclaration = type.BaseType as TypeRefDeclaration;
                
                if (baseAsTypeRefDeclaration != null)
                {
                    baseTypeDefDeclaration = baseAsTypeRefDeclaration.GetTypeDefinition();
                }
                else
                {
                    TypeSpecDeclaration baseAsTypeSpecDeclaration = type.BaseType as TypeSpecDeclaration;
                    baseTypeDefDeclaration = baseAsTypeSpecDeclaration.GetTypeDefinition();
                }
            }

            return baseTypeDefDeclaration;
        }

        private static MethodDefDeclaration FindOnPropertyChangedMethod(TypeDefDeclaration currentType)
        {
            foreach (MethodDefDeclaration declaration in currentType.Methods)
            {
                foreach (CustomAttributeDeclaration attributeType in declaration.CustomAttributes)
                {
                    if (attributeType.ConstructRuntimeObject().GetType().UnderlyingSystemType == typeof(OnPropertyChangedAttribute))
                    {
                        return declaration;
                    }
                }
            }

            return null;
        }
    }
}
